React-Redux 数据结构设计

什么数据适合放在 store 中?

我们可以通过数据持久度数据消费程度来区分数据类型:

  • 数据持久度

    • 快速变更型
    • 中等持续型
    • 长远稳定型

    快速变更型:这类数据往往代表着短时间内快速变更,比如文本框内容可能随着用户输入持续变化,或者快速拖动变更位置等,这类数据更加适合维护在 state 中。

    中等持续性:当用户浏览或者使用应用时,在刷新页面之前数据保持相对稳定,比如 ajax 获取数据,编辑 form 表单等,这类数据比较通用,可能被其他组件所应用。这类数据适合通过 Redux store 维护,再通过 connect 被组件使用。

    长远稳定型:指在页面多次刷新或者多次访问期间都保持不变的数据,这类数据通常不会放在 Redux 里面维护,一般会放到 localstorage 或者 db 里面。

  • 数据消费程度

    数据特性体现在消费层面,即有多少组件需要使用。越多组件消费的数据,就应该放在 Redux store 里面维护,反之,当数据只服务于单一组件时,由 React state 维护就更加合理。

假设现在将所有状态都放置在 Redux store 中管理:

  • 对于简单应用的状态,React state 管理已经绰绰有余,如果将所有状态放在 Redux store 中会带来额外的开发成本
  • 对于复杂应用的状态,将所有状态都放到 Redux store 中管理,会使 Redux store 状态树变得十分臃肿,维护变得十分困难,另一方面,对于层级较深的组件,它们与 Redux store 中的状态通信会变得复杂。

综上所述,不建议将所有状态都托管于 Redux store

数据不可变 (immutable or immer)

不可变数据对于 React 组件的意义不再赘述。这里想谈下两个优秀的库 immutableimmer

immutable 对象与原生的 js 对象操作有很大异同,加上繁琐的 fromJS 和 toJS 操作,会在一定程度上提升开发成本,并且 immutable 对项目侵入程度较大,会增加项目后期的维护成本。

可以考虑用支持原生 js 对象操作的 immer 库代替 immutable

数据扁平化

业务数据的原子性与原始性

  • 原子性
    保证业务数据的原子性是指不要将业务数据与 ui 数据或者其他业务数据(不同接口返回的数据)耦合,即不要将 ui 状态与其他业务数据掺杂进某个业务数据中。由于不同业务数据之间、业务数据与 ui 状态数据之间拥有不同的生命周期,如果混合使用,会给开发带来额外的开发和维护成本,举两个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// good! artilce and loading seperate
const state = {
// ui
loading: false,
// domain
article: {
title: 'xxxx',
name: 'xxxxx'
}
}
// bad! article and loading coupling
const state = {
article: {
loading: false,
title: 'xxxxx',
name: 'xxxx'
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// good! list and checked seperate
const state = {
// domain
list: [{data: 1}, {data: 2}],
// ui
checkedIndex: [1]
}
// bad! list and checked coupling
const state = {
list: [{data: 1, checked: true}, {data: 2}]
}
  • 原始性
    保证业务数据的原始性是指存放在 Redux store 中的数据应该是后端接口返回的原始数据,前端开发者不在 Redux 层面对其作任何处理,这么做的好处在于,原始的业务数据对不同组件的适配性更强,比如组件 A 需要一个正向排序的列表,组件 B 却需要一个逆向排序的列表,组件 C 则需要未排序的原始列表,较好的做法是分别在组件 A 和 组件 B 中对原始列表进行相应的排序,组件 C 则直接使用原始列表即可。

    上述组件 A、B、C 依赖于不同排序的数据列表,保证 Redux store 中该数据列表的原始性是比较好的做法,即使现在只有 A 组件需要正向排序后的数据列表,我也建议开发者不要将排序后的列表存于 Redux store 中,因为无法保证后续的需求迭代不会出现其他诸如 B、C 的组件依赖该列表,而且依赖方式可能各有不同。

    结论:保证业务数据的原始性有助于业务数据去适配对数据要求不一样的组件,适配逻辑可以考虑放到 mapStateToProps 函数中处理。

数据逻辑在哪写?

React-Redux 应用中,有四处地方可以写数据逻辑:

  • React 组件内部
  • action-creator 函数内部
  • reducer 函数内部
  • connect 函数内部

    React 组件内部写逻辑:如果组件内部需要维护 state,那么很可能会涉及到 state 的逻辑处理;而对于没有 state 可维护的组件而言,拿到手的 props 不一定可以直接适配 render 组件树需要的数据,因此需要对其进行衍生计算;甚至对于一些 需要在 action-creator 内部处理的逻辑也可以在组件内部进行消化。

    action-creator 函数内部reducer 函数内部写逻辑:action-creatorreducer 数据处理能力更强。得益于中间件action-creator 可以感知整个 Redux store,并且可以处理异步逻辑;而 reducer 只能获取自身维护的 state,并且受限于纯函数无法处理异步逻辑reducer 适合处理接收相同 action 后必须执行的通用逻辑,对于 reducer 可能出现过于冗长函数的问题,可以考虑使用 type-to-reducer 库来对不同 action 对应处理逻辑进一步拆分。

    connect 函数内部写逻辑:connect 作为 Redux storeReact 组件的胶合层,可以方便 React 组件获取 Redux store 衍生计算(通常在 mapStateToProps 函数中计算)后的状态,相比于在 render 函数中写 Redux store 衍生计算逻辑,在 mapStateToProps 函数中写逻辑可以在一定程度上将该部分逻辑与 render 的视图分离,分层更加清晰,并且可以通过 reselect 库可以缓存计算结果。

无论在哪写数据逻辑都应该遵循代码简洁、可读、可维护、可扩展的原则。

不建议将所有数据逻辑集中写在上述任何一个地方,如果你将数据合理的划分到组件自身的 stateReduxreduer 中,自然地,数据逻辑处理是分散的,不会集中在任何一个地方。